iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
Build on Google AI

30 天玩轉 Google AI 全家桶:初學者的隨身 Coding 助理養成記系列 第 25 篇

Day 25:紅綠燈驗證!看著單元測試從 Fail 到 Pass

  • 分享至 

  • xImage
  •  

昨天,DevPulse 的 patchFlow 已經會把 Gemini 產生的補丁套用到原始碼,並將結果寫進 fixed/。三個範例分別是 async-await.cjs、sql.cjs 和 boundary.cjs。

但「補丁成功寫進檔案」不等於「Bug 已經修好」。今天先不增加新功能,而是把 Jest 的測試目標切換到 fixed/,用原本的測試條件驗收 AI 產生的修復版本。

今天做三件事:

  1. 讓同一組測試可以選擇測試 samples/ 或 fixed/。
  2. 先跑原始檔,確認三個紅燈,再跑修復檔。
  3. 如果有測試仍然失敗,就從錯誤訊息與 diff 找出補丁的問題,而不是修改測試標準來製造綠燈。

一、先記住:補丁套用成功,不代表修復成功

Day 22 準備了三個故意寫錯的案例:

範例 原本要驗證的問題 測試預期
async-await.cjs forEach() 不會等待裡面的 async callback 所有工作完成後,回傳完整結果陣列
sql.cjs 把使用者輸入直接拼進 SQL 字串 SQL 使用 ? 佔位符,資料另外放進參數陣列
boundary.cjs profile 不存在時直接存取 name 會報錯 缺少 profile 時回傳 Unknown

這些預期行為就是驗收標準。今天不能因為 AI 改不過,就把 expect() 改成符合錯誤結果;否則只是讓測試配合 Bug,而不是讓程式符合需求。

二、讓測試在 samples/ 與 fixed/ 之間切換

目前三個測試檔都直接匯入 samples/。如果要比較原始碼與修復版,最簡單的方法是讓匯入路徑由環境變數決定:預設測試原始檔,只有明確設定 DEVPULSE_SOURCE_DIR=fixed 時,才改測 fixed/。

以 tests/async-await.test.cjs 為例,把原本的匯入:

const { sendAll } = require("../samples/async-await.cjs");

改成:

const sourceDir = process.env.DEVPULSE_SOURCE_DIR || "samples";
const { sendAll } = require(`../${sourceDir}/async-await.cjs`);

tests/sql.test.cjs 也採用同一個方式:

const sourceDir = process.env.DEVPULSE_SOURCE_DIR || "samples";
const { findUser } = require(`../${sourceDir}/sql.cjs`);

tests/boundary.test.cjs 則是:

const sourceDir = process.env.DEVPULSE_SOURCE_DIR || "samples";
const { getUserName } = require(`../${sourceDir}/boundary.cjs`);

只修改匯入路徑,保留原本的測試內容與 expect()。 這樣兩次測試使用的是同一套驗收條件,唯一差別是載入的程式碼來自 samples/ 還是 fixed/。

專案目錄會像這樣:

devpulse/
├─ samples/
│  ├─ async-await.cjs
│  ├─ sql.cjs
│  └─ boundary.cjs
├─ fixed/
│  ├─ async-await.cjs
│  ├─ sql.cjs
│  └─ boundary.cjs
└─ tests/
   ├─ async-await.test.cjs
   ├─ sql.test.cjs
   └─ boundary.test.cjs

執行前先確認 Day 24 的 patchFlow 已經為三個案例產生對應的修復檔。如果 fixed/ 裡少了某個檔案,Jest 會因為找不到模組而失敗;這和測試發現程式行為不符合預期,是兩種不同的問題。

三、先跑原始檔:紅燈是驗收基準

先在終端機執行:

npm test

因為沒有設定 DEVPULSE_SOURCE_DIR,測試預設載入 samples/。這三個檔案本來就是刻意保留的錯誤案例,所以預期會看到三個測試套件失敗。

FAIL tests/async-await.test.cjs
FAIL tests/sql.test.cjs
FAIL tests/boundary.test.cjs

Test Suites: 3 failed, 3 total
Tests:       3 failed, 3 total

以上是輸出示意,實際格式與錯誤細節會依 Jest 版本及測試內容而略有不同。發文時請換成自己的終端機截圖,並確認畫面確實顯示三個原始案例都失敗。

先看到紅燈反而是好事:它證明測試有能力抓出我們預先定義的問題。接下來才讓同一組測試去檢查 AI 的修復結果。

四、切換到 fixed/:看看 AI 有沒有真的修好

如果使用 Windows PowerShell,設定環境變數後再執行測試:

$env:DEVPULSE_SOURCE_DIR = "fixed"
npm test

如果使用 macOS 或 Linux 的終端機,可以執行:

DEVPULSE_SOURCE_DIR=fixed npm test

這次測試載入的是 fixed/ 裡由 patchFlow 產生的版本。若三個補丁都符合原本的測試契約,預期結果會變成:

PASS tests/async-await.test.cjs
PASS tests/sql.test.cjs
PASS tests/boundary.test.cjs

Test Suites: 3 passed, 3 total
Tests:       3 passed, 3 total

這同樣是預期輸出示意,並不是預先保證 Gemini 每次都會產生正確補丁。請以實際執行結果為準:如果有案例仍然是 FAIL,那就是今天最值得記錄的結果,不要為了得到綠燈而更改測試預期。

三個測試分別在確認不同的行為:

  • 非同步流程: sendAll() 必須等工作完成,再回傳完整的結果。可使用正確的 for...of 與 await,或以 Promise.all() 等待全部工作完成。
  • SQL 查詢: findUser() 應呼叫 db.query(sql, [userName]),不能只把 ? 放進 SQL 字串,卻忘了傳入參數陣列。
  • 邊界防護: getUserName({}) 應依照測試契約回傳 Unknown,而不是因為 profile 不存在就拋出例外。

五、如果還是紅燈,怎麼查?

AI 產生的補丁不一定每次都正確。若 fixed/ 仍有測試失敗,先看 Jest 指出的 Expected 與 Received,再檢查修復檔實際改了什麼。

例如,要比較 SQL 案例,可以執行:

git diff --no-index samples/sql.cjs fixed/sql.cjs

若要先檢查 JavaScript 語法,可以執行:

node --check fixed/sql.cjs

node --check 只能協助檢查語法,不能證明邏輯正確;真正的行為驗收仍然要靠 Jest。如果測試指出 SQL 的第二個參數遺漏,就回頭檢查 fixed/sql.cjs,而不是把測試改成只檢查 SQL 字串。

另外,若 PowerShell 已經設定過環境變數,之後想回到原始案例,可以執行:

Remove-Item Env:DEVPULSE_SOURCE_DIR
npm test

這會移除目前終端機的設定,讓測試再次預設載入 samples/。

今日學習踩坑小記

今天最重要的觀念是:測試要驗收程式,不是替程式找藉口。 Gemini 能分析問題,也能產生補丁,但它無法單靠「成功輸出檔案」證明修復正確。把測試固定下來,才有辦法客觀比較修復前後的差異。

如果三個案例都從 FAIL 變成 PASS,代表這三項已定義的行為符合預期;但這不代表整個專案沒有其他 Bug,也不等於完成完整的安全性稽核。測試能證明的範圍,取決於我們實際寫下的驗收條件。

下一篇 Day 26,會把目前的分析流程包裝成更方便的本機 CLI,讓 DevPulse 朝「用一行指令啟動分析」再前進一步。


Day 25 完成

  • ✅ 讓 Jest 可透過 DEVPULSE_SOURCE_DIR 切換測試目標
  • ✅ 用原本的 samples/ 重現三個紅燈
  • ✅ 用同一組測試驗收 fixed/ 裡的修復版本
  • ✅ 學會從 Jest 錯誤訊息與 git diff 檢查未通過的補丁
  • ✅ 確認「補丁有套用」與「修復有通過測試」是兩件事

上一篇
Day 24:撰寫 patchFlow,讓 Gemini 產出可以直接套用的補丁
下一篇
Day 26:打造本機 CLI:用單行指令呼叫你的 DevPulse
系列文
30 天玩轉 Google AI 全家桶:初學者的隨身 Coding 助理養成記 共 27 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言